#! /usr/bin/env python
# -*- coding: utf-8 -*-

# NumberX -- A mathematical puzzle game
#
# Copyright (C) 2009 Valéry Febvre <vfebvre@easter-eggs.com>
# http://code.google.com/p/numberx/
#
# This file is part of NumberX.
#
# NumberX is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# NumberX is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

"""
A mathematical puzzle game that will challenge your mental math abilities!

Four random numbers, between 1 and 9, you are given and you must put them
all together using the basic arithmetic operators in such a way as to
arrive at a given target random number.

Every puzzle has a solution.
The difficulty of the puzzles varies, some are easy and others are much
more difficult!
"""

import os, random, decimal
import elementary

APP_VERSION = '1.0.1'
APP_NAME = 'NumberX'

class Puzzle():
    def __init__(self):
        self.numbers = None
        self.operators = None
        self.target = None

        for i in range(100):
            self.generate()
            if self.validate():
                #print 'try', i
                break

    def __str__(self):
        numbers = self.numbers[:]
        for parenthese in self.parenthesis:
            numbers[parenthese[0]] = '(%s' % numbers[parenthese[0]]
            numbers[parenthese[1]] = '%s)' % numbers[parenthese[1]]
        # TODO: remove unnecessary parenthesis

        return ('%s %%s %s %%s %s %%s %s' % tuple(numbers)) % tuple(self.operators)

    def generate(self):
        random.seed()

        self.target = None

        self.numbers = []
        for i in range(4):
            self.numbers.append(str(random.randint(1, 9)))

        self.operators = []
        for i in range(3):
            self.operators.append(random.choice(('+', '-', '*', '/')))

        # start: 1+2+3+4
        #
        # (1+2)+3+4 => (1+2)+3+4 or (1+2)+(3+4)
        # (1+2+3)+4 => ((1+2)+3)+4 or (1+(2+3))+4
        # 1+(2+3)+4
        # 1+(2+3+4) => 1+((2+3)+4) or 1+(2+(3+4))
        # 1+2+(3+4)

        self.parenthesis = []
        r = random.randint(0, 3)
        if r == 1:
            r = random.randint(1, 2)
            self.parenthesis.append([0, r])
            if r == 1:
                r = random.randint(0, 1)
                if r == 1:
                    self.parenthesis.append([2, 3])
            elif r == 2:
                r = random.randint(0, 2)
                if r == 1:
                    self.parenthesis.append([0, 1])
                elif r == 2:
                    self.parenthesis.append([1, 2])
        elif r == 2:
            r = random.randint(2, 3)
            self.parenthesis.append([1, r])
            if r == 3:
                r = random.randint(0, 2)
                if r == 1:
                    self.parenthesis.append([1, 2])
                elif r == 2:
                    self.parenthesis.append([2, 3])
        elif r == 3:
            self.parenthesis.append([2, 3])

    def validate(self):
        numbers = self.numbers[:]
        for parenthese in self.parenthesis:
            numbers[parenthese[0]] = '(decimal.Decimal(%s)' % numbers[parenthese[0]]
            numbers[parenthese[1]] = 'decimal.Decimal(%s))' % numbers[parenthese[1]]
        for i, number in enumerate(numbers):
            if number.find('decimal') == -1:
                numbers[i] = 'decimal.Decimal(%s)' % number

        exp = ('%s %%s %s %%s %s %%s %s' % tuple(numbers)) % tuple(self.operators)
        try:
            target = eval(exp)
        except:
            return False
        else:
            if target > 0 and target == int(target):
                self.target = int(target)
                self.numbers_shuffled = self.numbers[:]
                random.shuffle(self.numbers_shuffled)
                return True
            else:
                return False


class NumberX(object):
    def __init__(self):
        self.win = elementary.Window(APP_NAME, elementary.ELM_WIN_BASIC)
        self.win.title_set(APP_NAME)
        self.win.callback_destroy_add(self.quit)

        bg = elementary.Background(self.win)
        self.win.resize_object_add(bg)
        bg.size_hint_weight_set(1, 1)
        bg.show()

        box_main = elementary.Box(self.win)
        box_main.size_hint_weight_set(1, 1)
        box_main.size_hint_align_set(-1, -1)
        self.win.resize_object_add(box_main)
        box_main.show()

        scroller = elementary.Scroller(self.win)
        scroller.bounce_set(False, False)
        scroller.size_hint_weight_set(1.0, 1.0)
        scroller.size_hint_align_set(-1.0, -1.0)
        box_main.pack_end(scroller)
        scroller.show()

        box = elementary.Box(self.win)
        box.size_hint_weight_set(1, 1)
        box.size_hint_align_set(-1, -1)
        scroller.content_set(box)
        box.show()

        #
        # actions: new, resolve, about, quit
        #
        box_actions = elementary.Box(self.win)
        box_actions.size_hint_weight_set(1, 0)
        box_actions.size_hint_align_set(-1, 0)
        box_actions.horizontal_set(True)
        box_actions.homogenous_set(True)
        box_actions.show()
        # New
        bt = elementary.Button(self.win)
        bt.label_set("New")
        bt.size_hint_weight_set(1, 1)
        bt.size_hint_align_set(-1.0, -1.0)
        bt.callback_clicked_add(self.new_puzzle)
        box_actions.pack_end(bt)
        bt.show()
        # Resolve
        bt = elementary.Button(self.win)
        bt.label_set("Resolve")
        bt.size_hint_weight_set(1, 1)
        bt.size_hint_align_set(-1.0, -1.0)
        bt.callback_clicked_add(self.resolve_puzzle)
        box_actions.pack_end(bt)
        bt.show()
        # About
        bt = elementary.Button(self.win)
        bt.label_set("About")
        bt.size_hint_weight_set(1, 1)
        bt.size_hint_align_set(-1.0, -1.0)
        bt.callback_clicked_add(self.open_about)
        box_actions.pack_end(bt)
        bt.show()
        # Quit
        bt = elementary.Button(self.win)
        bt.label_set("Quit")
        bt.size_hint_weight_set(1, 1)
        bt.size_hint_align_set(-1.0, -1.0)
        bt.callback_clicked_add(self.quit)
        box_actions.pack_end(bt)
        bt.show()
        box.pack_end(box_actions)

        #
        # target
        #
        box_target = elementary.Box(self.win)
        box_target.size_hint_weight_set(1, 1)
        box_target.size_hint_align_set(-1, 0.5)
        box_target.horizontal_set(True)
        box_target.show()
        self.label_target = elementary.Label(self.win)
        self.label_target.scale_set(1.5)
        self.label_target.size_hint_weight_set(0, 0)
        self.label_target.size_hint_align_set(0.5, 0)
        box_target.pack_end(self.label_target)
        self.label_target.show()
        box.pack_end(box_target)

        #
        # entry
        #
        box_entry = elementary.Box(self.win)
        box_entry.scale_set(2)
        box_entry.size_hint_weight_set(0, 0)
        box_entry.size_hint_align_set(-1, -1)
        box_entry.horizontal_set(True)
        box_entry.show()

        scroller = elementary.Scroller(self.win)
        scroller.size_hint_weight_set(1, 1)
        scroller.size_hint_align_set(-1, -1)
        scroller.bounce_set(True, False)
        box_entry.pack_end(scroller)
        scroller.show()

        self.entry = elementary.Entry(self.win)
        self.entry.single_line_set(True)
        self.entry.editable_set(False)
        self.entry.size_hint_weight_set(1, 1)
        self.entry.size_hint_align_set(-1, -1)
        scroller.content_set(self.entry)
        self.entry.show()

        self.label_value = elementary.Label(self.win)
        self.label_value.show()
        box_entry.pack_end(self.label_value)

        box.pack_end(box_entry)

        #
        # 4 numbers
        #
        box_numbers = elementary.Box(self.win)
        box_numbers.size_hint_weight_set(1, 1)
        box_numbers.size_hint_align_set(-1, -1)
        box_numbers.horizontal_set(True)
        box_numbers.show()
        self.btns_numbers = []
        # 1st
        bt = elementary.Button(self.win)
        bt.scale_set(2)
        bt.size_hint_weight_set(1, 1)
        bt.size_hint_align_set(-1.0, -1.0)
        bt.callback_clicked_add(self.entry_insert, 0)
        box_numbers.pack_end(bt)
        bt.show()
        self.btns_numbers.append({'widget': bt, 'label': None})
        # 2nd
        bt = elementary.Button(self.win)
        bt.scale_set(2)
        bt.size_hint_weight_set(1, 1)
        bt.size_hint_align_set(-1.0, -1.0)
        bt.callback_clicked_add(self.entry_insert, 1)
        box_numbers.pack_end(bt)
        bt.show()
        self.btns_numbers.append({'widget': bt, 'label': None})
        # 3rd
        bt = elementary.Button(self.win)
        bt.scale_set(2)
        bt.size_hint_weight_set(1, 1)
        bt.size_hint_align_set(-1.0, -1.0)
        bt.callback_clicked_add(self.entry_insert, 2)
        box_numbers.pack_end(bt)
        bt.show()
        self.btns_numbers.append({'widget': bt, 'label': None})
        # 4th
        bt = elementary.Button(self.win)
        bt.scale_set(2)
        bt.size_hint_weight_set(1, 1)
        bt.size_hint_align_set(-1.0, -1.0)
        bt.callback_clicked_add(self.entry_insert, 3)
        box_numbers.pack_end(bt)
        bt.show()
        box.pack_end(box_numbers)
        self.btns_numbers.append({'widget': bt, 'label': None})

        #
        # operators and parenthesis: +, -, *, /, (, )
        #
        box_operators = elementary.Box(self.win)
        box_operators.size_hint_weight_set(1, 1)
        box_operators.size_hint_align_set(-1, -1)
        box_operators.horizontal_set(True)
        box_operators.show()
        # +
        bt = elementary.Button(self.win)
        bt.scale_set(2)
        bt.label_set("+")
        bt.size_hint_weight_set(1, 1)
        bt.size_hint_align_set(-1.0, -1.0)
        bt.callback_clicked_add(self.entry_insert, '+')
        box_operators.pack_end(bt)
        bt.show()
        # -
        bt = elementary.Button(self.win)
        bt.scale_set(2)
        bt.label_set("-")
        bt.size_hint_weight_set(1, 1)
        bt.size_hint_align_set(-1.0, -1.0)
        bt.callback_clicked_add(self.entry_insert, '-')
        box_operators.pack_end(bt)
        bt.show()
        # (
        bt = elementary.Button(self.win)
        bt.scale_set(2)
        bt.label_set("(")
        bt.size_hint_weight_set(1, 1)
        bt.size_hint_align_set(-1.0, -1.0)
        bt.callback_clicked_add(self.entry_insert, '(')
        box_operators.pack_end(bt)
        bt.show()
        # )
        bt = elementary.Button(self.win)
        bt.scale_set(2)
        bt.label_set(")")
        bt.size_hint_weight_set(1, 1)
        bt.size_hint_align_set(-1.0, -1.0)
        bt.callback_clicked_add(self.entry_insert, ')')
        box_operators.pack_end(bt)
        bt.show()
        # *
        bt = elementary.Button(self.win)
        bt.scale_set(2)
        bt.label_set("*")
        bt.size_hint_weight_set(1, 1)
        bt.size_hint_align_set(-1.0, -1.0)
        bt.callback_clicked_add(self.entry_insert, '*')
        box_operators.pack_end(bt)
        bt.show()
        # /
        bt = elementary.Button(self.win)
        bt.scale_set(2)
        bt.label_set("/")
        bt.size_hint_weight_set(1, 1)
        bt.size_hint_align_set(-1.0, -1.0)
        bt.callback_clicked_add(self.entry_insert, '/')
        box_operators.pack_end(bt)
        bt.show()
        box.pack_end(box_operators)

        #
        # actions: clear, backspace
        #
        box_entry_actions = elementary.Box(self.win)
        box_entry_actions.size_hint_weight_set(1, 0)
        box_entry_actions.size_hint_align_set(-1, 0)
        box_entry_actions.horizontal_set(True)
        box_entry_actions.homogenous_set(True)
        box_entry_actions.show()
        # Clear
        bt = elementary.Button(self.win)
        bt.label_set("Clear")
        bt.size_hint_weight_set(1, 1)
        bt.size_hint_align_set(-1.0, -1.0)
        bt.callback_clicked_add(self.entry_clear)
        box_entry_actions.pack_end(bt)
        bt.show()
        # Backspace
        bt = elementary.Button(self.win)
        bt.label_set("Backspace")
        bt.size_hint_weight_set(1, 1)
        bt.size_hint_align_set(-1.0, -1.0)
        bt.callback_clicked_add(self.entry_backspace)
        box_entry_actions.pack_end(bt)
        bt.show()
        box.pack_end(box_entry_actions)

        self.new_puzzle()
        self.win.resize(480, 640)
        self.win.show()

    def new_puzzle(self, *args):
        self.puzzle = Puzzle()

        self.label_target.label_set('Your target is %s' % self.puzzle.target)
        self.label_value.label_set(' = ?')
        self.entry.entry_set('')
        for i, number in enumerate(self.puzzle.numbers_shuffled):
            self.btns_numbers[i]['widget'].label_set(number)
            self.btns_numbers[i]['widget'].scale_set(2)
            self.btns_numbers[i]['label'] = number

    def resolve_puzzle(self, button):
        self.entry.entry_set(str(self.puzzle))
        self.label_value.label_set(' = %d' % self.puzzle.target)

    def entry_clear(self, button):
        self.entry.entry_set('')
        self.label_value.label_set(' = ?')
        for btn in self.btns_numbers:
            btn['widget'].scale_set(2)

    def entry_insert(self, button, data):
        exp = self.entry.entry_get().replace('<br>', '')

        if data in [0, 1, 2, 3]:
            # use widget scale value to know if a number has been already used
            if button.scale_get() == 1:
                return
            # only append a number after an operator
            if exp and exp.strip()[-1] not in ['+', '-', '*', '/', '(']:
                return
            data = self.puzzle.numbers_shuffled[data]
            button.scale_set(1)

        if data == '(':
            if exp and exp.strip()[-1] not in ['+', '-', '*', '/', '(']:
                return
        if data == ')':
            if exp and exp.strip()[-1] in ['+', '-', '*', '/', '(']:
                return

        if data in ['+', '-', '*', '/']:
            if exp and exp.strip()[-1] in ['+', '-', '*', '/', '(']:
                return
            data = ' %s ' % data

        exp = exp + data
        self.entry.entry_set(exp)
        self.entry_eval(exp)

    def entry_eval(self, exp):
        try:
            for number in self.puzzle.numbers:
                exp = exp.replace(number, 'decimal.Decimal(%s)' % number)
            value = eval(exp)
            if value == int(value):
                self.label_value.label_set(' = ' + str(eval(exp)))
            else:
                self.label_value.label_set(' = %.2f' % float(eval(exp)))
        except:
            self.label_value.label_set(' = ?')
            self.label_target.label_set('Your target is %s' % self.puzzle.target)
        else:
            if eval(exp) == self.puzzle.target:
                resolved = True
                for number in self.puzzle.numbers:
                    if exp.count(number) != self.puzzle.numbers.count(number):
                        resolved = False
                        break
                if resolved:
                    self.label_target.label_set('Target %s reached. Bravo!!!' % self.puzzle.target)

    def entry_backspace(self, button):
        exp = self.entry.entry_get().replace('<br>', '')
        if not exp:
            return
        if exp[-1] == ' ':
            exp = exp[:-3]
        else:
            number = exp[-1]
            for btn in self.btns_numbers:
                if btn['label'] == number and btn['widget']. scale_get() != 2:
                    btn['widget'].scale_set(2)
                    break
            exp = exp[:-1]
        self.entry.entry_set(exp)
        self.entry_eval(exp)

    def open_about(self, button):
        self.iw_about = elementary.InnerWindow(self.win)
        self.iw_about.show()

        box = elementary.Box(self.win)
        self.iw_about.content_set(box)
        box.show()

        scroller = elementary.Scroller(self.win)
        scroller.bounce_set(False, True)
        scroller.size_hint_weight_set(1.0, 1.0)
        scroller.size_hint_align_set(-1.0, -1.0)
        box.pack_end(scroller)
        scroller.show()

        about = """\
<b>NumberX</> is a mathematical puzzle game that will challenge your mental math abilities!

Four random numbers, between 1 and 9, you are given and you must put them all together using the basic arithmetic operators in such a way as to arrive at a given target random number.

Every puzzle has a solution.

The difficulty of the puzzles varies, some are easy and others are much more difficult!

<b>Copyright</> © 2009 Valéry Febvre
<b>Licensed</> under the GNU GPL v3
<b>Homepage</> http://code.google.com/p/numberx/

If you like this program send me an email to vfebvre@easter-eggs.com\
"""

        entry = elementary.Entry(self.win)
        entry.editable_set(False)
        entry.line_wrap_set(True)
        entry.size_hint_weight_set(1, 1)
        entry.entry_set(about.replace('\n', '<br>'))
        scroller.content_set(entry)
        entry.show()

        bt = elementary.Button(self.win)
        bt.size_hint_weight_set(1, 0)
        bt.size_hint_align_set(-1, 0)
        bt.label_set('Close')
        bt.callback_clicked_add(self.close_about)
        box.pack_end(bt)
        bt.show()

    def close_about(self, button):
        self.iw_about.delete()

    def quit(self, *args):
        elementary.exit()


def main():
    elementary.init()
    NumberX()
    elementary.run()
    elementary.shutdown()
    return 0

if __name__ == "__main__":
    exit(main())
